﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using log4net;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using SharePoint.Orm.Attributes;
using SharePoint.Orm.Helpers;
using SharePoint.Orm.ItemProxy;
using SharePoint.Orm.Wrappers;
using Microsoft.SharePoint.Utilities;

namespace SharePoint.Orm {
	/// <summary>
	/// Provides a common set of methods to manage and access SharePoint data.
	/// </summary>
	public class SPDataContext : IDisposable {
		private static readonly ILog _log = LogManager.GetLogger(typeof(SPDataContext));
		private readonly SPSite _web;
		private readonly SPWeb _site;
		private readonly bool _disposeWeb;
		private readonly bool _disposeSite;
		private readonly IValueTypeConverter _valueTypeConverter;
		private readonly List<Type> _listTypes = new List<Type>();
		internal Action<SPDataContext> Disposing;

		/// <summary>
		/// Opens a <see cref="SPWeb"/> using the given URL. The <see cref="SPWeb"/> will be disposed of when this
		/// object is disposed.
		/// </summary>
		/// <param name="url">The URL of the SharePoint site.</param>
		/// <param name="dataContextOptions">Options used for this data context.</param>
		public SPDataContext(string url, SPDataContextOptions dataContextOptions) {
			if (dataContextOptions == null) {
				dataContextOptions = new SPDataContextOptions();
			}
			_web = new SPSite(url);
			_site = _web.OpenWeb();
			_disposeWeb = true;
			_disposeSite = true;
			_valueTypeConverter = dataContextOptions.ValueTypeConverter;
		}

		/// <summary>
		/// Opens a <see cref="SPWeb"/> using the given URL. The <see cref="SPWeb"/> will be disposed of when this
		/// object is disposed.
		/// </summary>
		/// <param name="url">The URL of the SharePoint site.</param>
		public SPDataContext(string url)
			: this(url, null) {
		}

		/// <summary>
		/// Uses the <see cref="SPWeb"/> given to access SharePoint.
		/// </summary>
		/// <param name="site">An already open <see cref="SPWeb"/></param>
		/// <param name="dataContextOptions">Options used for this data context.</param>
		public SPDataContext(SPWeb site, SPDataContextOptions dataContextOptions) {
			if (dataContextOptions == null) {
				dataContextOptions = new SPDataContextOptions();
			}
			_disposeWeb = false;
			_disposeSite = false;
			_site = site;
			_valueTypeConverter = dataContextOptions.ValueTypeConverter;
		}

		/// <summary>
		/// Uses the <see cref="SPWeb"/> given to access SharePoint.
		/// </summary>
		/// <param name="site">An already open <see cref="SPWeb"/></param>
		public SPDataContext(SPWeb site)
			: this(site, null) {
		}

		/// <summary>
		/// The <see cref="SPWeb"/> used to access SharePoint.
		/// </summary>
		public virtual SPWeb Site {
			get { return _site; }
		}

		/// <summary>
		/// The <see cref="IValueTypeConverter"/> used when reading and writing data from an <see cref="SPListItem"/>.
		/// </summary>
		public IValueTypeConverter ValueTypeConverter {
			get { return _valueTypeConverter; }
		}

		protected virtual SPQueryable<TList> CreateList<TList>() where TList : ISPOrmList {
			RegisterList(typeof(TList));
			return new SPQueryable<TList>(this);
		}

		/// <summary>
		/// Gets a queryable data source given an ORM list type.
		/// </summary>
		/// <typeparam name="TList">An ORM list type.</typeparam>
		/// <returns>A queryable data source.</returns>
		public virtual IQueryable GetList<TList>() {
			return new SPQueryable<TList>(this);
		}

		protected virtual void RegisterList(Type listType) {
			_listTypes.Add(listType);
		}

		/// <summary>
		/// Get the <see cref="SPListItem"/> from an ORM content type object or ORM list object.
		/// </summary>
		/// <param name="obj">ORM content type object or ORM list object.</param>
		/// <returns>The <see cref="SPListItem"/> which is attached to the ORM object.</returns>
		public virtual SPListItem GetSPListItem(object obj) {
			if (obj == null) {
				throw new ArgumentNullException("obj");
			}
			if (obj is SPListItem) {
				return (SPListItem)obj;
			}
			ISPListItemAware listItemAware = obj as ISPListItemAware;
			if (listItemAware != null) {
				return listItemAware.ListItem;
			}
			throw new SPOrmException("Could not get SPListItem from type '" + obj.GetType() + "'");
		}

		/// <summary>
		/// Gets the <see cref="SPContentType"/> for which the ORM content type fronts.
		/// </summary>
		/// <typeparam name="TContentType">The ORM content type.</typeparam>
		/// <returns>The <see cref="SPContentType"/>.</returns>
		public virtual SPContentType GetSPContentType<TContentType>() where TContentType : ISPOrmContentType {
			SPOrmContentTypeAttribute contentTypeAttr = SPOrmAttributeHelper.GetContentTypeAttribute<TContentType>();
			return _site.ContentTypes[contentTypeAttr.Name];
		}

		/// <summary>
		/// Gets the <see cref="SPList"/> for which the ORM list type fronts.
		/// </summary>
		/// <typeparam name="TList">The ORM list type.</typeparam>
		/// <returns>The <see cref="SPList"/>.</returns>
		public virtual SPList GetSPList<TList>() where TList : ISPOrmList {
			SPOrmListAttribute listAttr = SPOrmAttributeHelper.GetListAttribute<TList>();
            try {
                return _site.Lists[listAttr.Title];
            }catch(ArgumentException) {
                var similarLists = string.Join(" ,",_site.Lists.Cast<SPList>().Select(l => l.Title).Where(t => Levenshtein.Compute(t,listAttr.Title) < 10).ToArray());
                throw new IndexOutOfRangeException("The list named " + listAttr.Title + " was not found in SharePoint. The closest lists were: " + similarLists);
            }
		}

		/// <summary>
		/// Executes a CAML query against the specified list.
		/// </summary>
		/// <typeparam name="TList">The ISPOrmList list type.</typeparam>
		/// <typeparam name="TItemType">The expected item types returned</typeparam>
		/// <param name="caml">The caml query to execute</param>
		/// <returns>An IEnumerable of the items.</returns>
		public virtual IEnumerable<TItemType> ExecuteCaml<TList, TItemType>(string caml) where TList : ISPOrmList {
			SPList list = GetSPList<TList>();
			SPQuery query = new SPQuery {
				Query = caml
			};
			// TODO: this logic should probably be merged with SPQueryContext.GetListItemEnumerable
			_log.Debug("executing caml query:\n" + query.Query);
			SPListItemCollection listItems = list.GetItems(query);
			_log.Debug("Query returned " + listItems.Count + " items");
			return new SPListItemConverterEnumerable<TItemType>(this, listItems);
		}

		/// <summary>
		/// Sends an email using the <see cref="SPUtility.SendEmail(Microsoft.SharePoint.SPWeb,bool,bool,string,string,string)"/> method.
		/// </summary>
		/// <param name="to">The to email address</param>
		/// <param name="subject">The subject of the message</param>
		/// <param name="htmlBody">The body of the message</param>
		/// <returns>True if email was sent successfully</returns>
		public virtual bool SendEmail(string to, string subject, string htmlBody) {
			_log.Debug("Sending Email\nTo: " + to + "\nSubject: " + subject + "\nBody:\n" + htmlBody);
			return SPUtility.SendEmail(Site, false, false, to, subject, htmlBody);
		}

		/// <summary>
		/// Sends an email using the <see cref="SPUtility.SendEmail(Microsoft.SharePoint.SPWeb,bool,bool,string,string,string)"/> method.
		/// </summary>
		/// <param name="user">The user to send email to</param>
		/// <param name="subject">The subject of the message</param>
		/// <param name="htmlBody">The body of the message</param>
		public virtual void SendEmail(ISPOrmUser user, string subject, string htmlBody) {
			_log.Debug("Sending Email\nTo: " + user.Email + "\nSubject: " + subject + "\nBody:\n" + htmlBody);
			SPUtility.SendEmail(Site, false, false, user.Email, subject, htmlBody);
		}

		/// <summary>
		/// Sends an email using the <see cref="SPUtility.SendEmail(Microsoft.SharePoint.SPWeb,bool,bool,string,string,string)"/> method
		/// to all the members of a group.
		/// </summary>
		/// <param name="group">The group to send emails to</param>
		/// <param name="subject">The subject of the message</param>
		/// <param name="htmlBody">The body of the message</param>
		public virtual void SendEmail(ISPOrmGroup group, string subject, string htmlBody) {
			_log.Debug("Sending Email\nTo: " + group.Name + " (group)\nSubject: " + subject + "\nBody:\n" + htmlBody);
			foreach (ISPOrmUser user in group.GetUsers()) {
				SPUtility.SendEmail(Site, false, false, user.Email, subject, htmlBody);
			}
		}

		/// <summary>
		/// Attaches an <see cref="SPListItem"/> to the context and returns a new ORM object wrapping that <see cref="SPListItem"/>.
		/// </summary>
		/// <typeparam name="TListOrContentType">An ORM list type or ORM content type.</typeparam>
		/// <param name="listItem">The <see cref="SPListItem"/> to attach.</param>
		/// <returns>The ORM object.</returns>
		public virtual TListOrContentType Attach<TListOrContentType>(SPListItem listItem) where TListOrContentType : ISPOrmObject {
			if (listItem == null) {
				throw new ArgumentNullException("listItem", "'listItem' cannot be null.");
			}
			return SPListItemProxyGenerator.Create<TListOrContentType>(this, listItem);
		}

		/// <summary>
		/// Deletes the <see cref="SPListItem"/> which is backing the ORM object.
		/// </summary>
		/// <param name="obj">The ORM object to delete.</param>
		public virtual void Delete(object obj) {
			SPListItem spListItem = GetSPListItem(obj);
			spListItem.Delete();
		}

		/// <summary>
		/// Deletes all <see cref="SPListItem"/> which match the LINQ query.
		/// </summary>
		/// <param name="objsToDelete">The list of objects to delete.</param>
		public virtual void DeleteAll(IQueryable objsToDelete) {
			// TODO don't iterate, build caml and delete
			List<object> list = new List<object>();
			foreach (object objToDelete in objsToDelete) {
				list.Add(objToDelete);
			}
			foreach (object obj in list) {
				Delete(obj);
			}
		}
		/// <summary>
		/// Saves a new ORM object into SharePoint. This will fail if the item is already attached.
		/// </summary>
		/// <typeparam name="TList">The list to save the item to.</typeparam>
		/// <typeparam name="TContentType">The content type of the item</typeparam>
		/// <param name="obj">The object to save.</param>
		/// <returns>The just saved entity</returns>
		public virtual TContentType Save<TList,TContentType>(TContentType obj) where TList : ISPOrmList where TContentType : ISPOrmContentType {
			SPListItem listItem = SaveListItem<TList>(obj);
			return Attach<TContentType>(listItem);
		}

		private SPListItem SaveListItem<TList>(object obj)
			where TList : ISPOrmList {
			ISPListItemAware listItemAware = obj as ISPListItemAware;
			if (listItemAware != null && listItemAware.ListItem != null) {
				throw new ArgumentException("The object you are trying to save already has a SPListItem attached. Try calling Update instead.", "obj");
			}
			if (obj.GetType().GetCustomAttributes(typeof(SPOrmListAttribute), true).FirstOrDefault() == null &&
			    obj.GetType().GetCustomAttributes(typeof(SPOrmContentTypeAttribute), true).FirstOrDefault() == null) {
				throw new ArgumentException("The object you are trying to save must have the SPOrmList or SPOrmContentType attributes.", "obj");
			}

			SPList spList = GetSPList<TList>();
			SPListItem listItem = spList.Items.Add();
			SPListItemHelper.PopulateFromDecoratedClass(listItem, this, obj);
			listItem.Update();
			return listItem;
		}


		/// <summary>
		/// Saves a new ORM object into SharePoint. This will fail if the item is already attached.
		/// </summary>
		/// <typeparam name="TList">The list to save the item to.</typeparam>
		/// <param name="obj">The object to save.</param>
		/// <returns>The <see cref="SPListItem"/> id.</returns>
		public virtual int Save<TList>(object obj) where TList : ISPOrmList {
			SPListItem listItem = SaveListItem<TList>(obj);
			return listItem.ID;
		}

		/// <summary>
		/// Updates an ORM object in SharePoint. This method requires that the ORM object is already attached.
		/// </summary>
		/// <param name="obj">The object to update.</param>
		public virtual void Update(object obj) {
			ISPListItemAware listItemAware = obj as ISPListItemAware;
			if (listItemAware == null) {
				throw new ArgumentException(obj.GetType() + " must implement " + typeof(ISPListItemAware).Name, "obj");
			}
			SPListItem listItem = listItemAware.ListItem;
			if (listItem == null) {
				throw new ArgumentException("The object does not have a SPListItem. You may need to call Save instead.", "obj");
			}
			listItem.Update();
		}

		public void Dispose() {
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing) {
			if (disposing) {
				if (Disposing != null) {
					Disposing(this);
				}
				if (_disposeSite) {
					_site.Dispose();
				}
				if (_disposeWeb) {
					_web.Dispose();
				}
			}
		}

		/// <summary>
		/// Gets a <see cref="IQueryable"/> wrapping a SharePoint <see cref="SPList"/>.
		/// </summary>
		/// <param name="listId">The list id of the SharePoint <see cref="SPList"/> to wrap.</param>
		/// <returns>The <see cref="IQueryable"/> wrapping the <see cref="SPList"/>.</returns>
		public virtual IQueryable GetListByListId(Guid listId) {
			foreach (Type listType in _listTypes) {
				SPOrmListAttribute listAttr = listType.GetCustomAttributes(typeof(SPOrmListAttribute), true).Cast<SPOrmListAttribute>().FirstOrDefault();
				if (listAttr != null) {
					SPList list = _site.Lists[listAttr.Title];
					if (list.ID == listId) {
						Type genericQueryable = typeof(SPQueryable<>).MakeGenericType(listType);
						return (IQueryable)Activator.CreateInstance(genericQueryable, this);
					}
				}
			}
			return null;
		}

		/// <summary>
		/// Gets an ORM object by id from a <see cref="IQueryable"/>.
		/// </summary>
		/// <param name="queryable">The <see cref="IQueryable"/> to get the ORM object from.</param>
		/// <param name="listItemId">The id of the item to get.</param>
		/// <returns>The ORM object with the given id. null, if not found.</returns>
		public virtual object GetListItemById(IQueryable queryable, int listItemId) {
			foreach (object o in queryable) {
				PropertyInfo idProperty = o.GetType().GetProperty("Id");
				if (idProperty != null && idProperty.PropertyType == typeof(int)) {
					int id = (int)idProperty.GetValue(o, null);
					if (listItemId == id) {
						return o;
					}
				}
			}
			return null;
		}

		/// <summary>
		/// Adds a file to a document library and returns the ORM object which was created.
		/// </summary>
		/// <typeparam name="TList">The document library to add the file to.</typeparam>
		/// <param name="fileName">The file name of the file to add.</param>
		/// <param name="stream">The <see cref="Stream"/> containing the contents of the new file.</param>
		/// <returns>The ORM object which was created.</returns>
		public virtual TList AddFile<TList>(string fileName, Stream stream) where TList : ISPOrmList {
			return AddFile<TList>(fileName, stream, documentLibrary => documentLibrary.RootFolder);
		}

		/// <summary>
		/// Adds a file to a document library and returns the ORM object which was created.
		/// </summary>
		/// <typeparam name="TList">The document library to add the file to.</typeparam>
		/// <param name="fileName">The file name of the file to add.</param>
		/// <param name="stream">The <see cref="Stream"/> containing the contents of the new file.</param>
		/// <param name="getFolder">Delegate used to get the folder to place the file in.</param>
		/// <returns>The ORM object which was created.</returns>
		public virtual TList AddFile<TList>(string fileName, Stream stream, Func<SPDocumentLibrary, SPFolder> getFolder) where TList : ISPOrmList {
			SPDocumentLibrary doclib = GetSPList<TList>() as SPDocumentLibrary;
			if (doclib == null) {
				throw new ArgumentException("TList is not a SPDocumentLibrary");
			}
			SPFolder folder = getFolder(doclib);
			SPFile file = folder.Files.Add(fileName, stream, true);
			return Attach<TList>(file.Item);
		}

		/// <summary>
		/// Casts one ORM object to another ORM object. This method is useful if you have an ORM list object
		/// and need an ORM content type object.
		/// </summary>
		/// <typeparam name="T">The type to cast the object to.</typeparam>
		/// <param name="obj">The object to cast.</param>
		/// <returns>The new ORM object.</returns>
		public virtual T Cast<T>(object obj) where T : ISPOrmObject {
			if (obj is ISPListItemAware) {
				SPListItem listItem = ((ISPListItemAware)obj).ListItem;
				return Attach<T>(listItem);
			}
			throw new ArgumentException("Cannot cast object unless it is a Orm object and has already been saved.", "obj");
		}

		/// <summary>
		/// Gets the current user associated with the site.
		/// </summary>
		/// <returns>The current user.</returns>
		public virtual ISPOrmUser GetCurrentUser() {
			return new SPOrmSharePointUser(Site.CurrentUser);
		}

		/// <summary>
		/// The site url.
		/// </summary>
		public virtual string Url {
			get { return Site.Url; }
		}

		/// <summary>
		/// Finds a user by id.
		/// </summary>
		/// <param name="id">The id of the user.</param>
		/// <returns>The user with id.</returns>
		public virtual ISPOrmUser FindUserByIdOrDefault(int id) {
			SPUser user = SPWebHelper.FindUserByIdOrDefault(Site, id);
			if (user == null) {
				return null;
			}
			return new SPOrmSharePointUser(user);
		}

		/// <summary>
		/// Finds a site group by name.
		/// </summary>
		/// <param name="name">The name of the group to find.</param>
		/// <returns>The group found or null.</returns>
		public virtual ISPOrmGroup FindSiteGroupByNameOrDefault(string name) {
			SPGroup group = SPWebHelper.FindSiteGroupByNameOrDefault(Site, name);
			if (group == null) {
				return null;
			}
			return new SPOrmSharePointGroup(group);
		}

		/// <summary>
		/// ensures the role assignment on the given object.
		/// </summary>
		/// <param name="obj">The ORM object to update.</param>
		/// <param name="ormPrincipal">The user or group to assign</param>
		/// <param name="roleDefinitions">The role definitions.</param>
		public virtual void SPOrmObjectFindOrAddRoleAssignment(object obj, ISPOrmPrincipal ormPrincipal, string[] roleDefinitions) {
			SPListItem item = GetSPListItem(obj);
			SPPrincipal principal = ormPrincipal.GetSPPrincipal();
			SPListItemHelper.FindOrAddRoleAssignment(item, Site, principal, roleDefinitions);
		}

		/// <summary>
		/// gets a list of role assignments
		/// </summary>
		/// <param name="obj">the SPOrm object to get the role assignments for</param>
		/// <returns>The list of role assignments</returns>
		public virtual IEnumerable<ISPOrmRoleAssignment> SPOrmObjectGetRoleAssignments(object obj) {
			SPListItem item = GetSPListItem(obj);
			return item.RoleAssignments
				.Cast<SPRoleAssignment>()
				.Select(ra => (ISPOrmRoleAssignment)new SPOrmSharePointRoleAssignment(ra));
		}

		/// <summary>
		/// removes a member from the role assignments
		/// </summary>
		/// <param name="obj">The SPOrm object to remove the role assignment from</param>
		/// <param name="member">The member to remove</param>
		public virtual void SPOrmObjectRemoveRoleAssignment(object obj, ISPOrmPrincipal member) {
			SPListItem item = GetSPListItem(obj);
			item.RoleAssignments.Remove(member.GetSPPrincipal());
		}

		/// <summary>
		/// breaks the role inheritance for the given item
		/// </summary>
		/// <param name="obj">The SPOrm object to break role inheritance on</param>
		/// <param name="copyRoleAssignments">true, to copy the role assignments from the parent</param>
		public virtual void SPOrmObjectBreakRoleInheritance(object obj, bool copyRoleAssignments) {
			SPListItem item = GetSPListItem(obj);
			item.BreakRoleInheritance(copyRoleAssignments);
		}

		/// <summary>
		/// Resets the role inheritance on an item.
		/// </summary>
		/// <param name="obj">The item to reset the role inheritance on.</param>
		public virtual void SPOrmObjectResetRoleInheritance(object obj) {
			SPListItem item = GetSPListItem(obj);
			item.ResetRoleInheritance();
		}

		/// <summary>
		/// Gets the active workflow task on a SPOrm object.
		/// </summary>
		/// <param name="obj">The object to get the active task on.</param>
		/// <returns>The active task or null if none.</returns>
		public virtual ISPOrmWorkflowTask SPOrmObjectWorkflowGetActiveTask(object obj) {
			SPListItem listItem = GetSPListItem(obj);
			SPWorkflowTask task = SPWorkflowHelper.GetActiveTask(listItem);
			if (task == null) {
				return null;
			}
			return new SPOrmSharePointWorkflowTask(this, task);
		}

		/// <summary>
		/// Gets the active workflow on a SPOrm object.
		/// </summary>
		/// <param name="obj">The object to get the active workflow</param>
		/// <returns>The workflow or null if not found</returns>
		public virtual ISPOrmWorkflow SPOrmObjectWorkflowGetActiveWorkflow(object obj) {
			SPListItem listItem = GetSPListItem(obj);
			SPWorkflow workflow = SPWorkflowHelper.GetActiveSPWorkflow(listItem);
			if (workflow == null) {
				return null;
			}
			return new SPOrmSharePointWorkflow(workflow);
		}

		/// <summary>
		/// Ensure that a workflow is started on an object.
		/// </summary>
		/// <param name="obj">The object to ensure the workflow is started on.</param>
		/// <param name="workflowName">The name of the workflow to start.</param>
		public virtual void SPOrmObjectWorkflowEnsureStarted(object obj, string workflowName) {
			SPListItem listItem = GetSPListItem(obj);
			SPWorkflowManager workflowManager = Site.Site.WorkflowManager;
			SPWorkflowHelper.EnsureStarted(workflowManager, listItem, workflowName);
		}

		/// <summary>
		/// Ensure that a workflow is started on an object.
		/// </summary>
		/// <param name="obj">The object to ensure the workflow is started on.</param>
		/// <param name="workflowName">The name of the workflow to start.</param>
		/// <param name="eventData">The event data to start the workflow with.</param>
		public virtual void SPOrmObjectWorkflowEnsureStarted(object obj, string workflowName, string eventData) {
			SPListItem listItem = GetSPListItem(obj);
			SPWorkflowManager workflowManager = Site.Site.WorkflowManager;
			SPWorkflowHelper.EnsureStarted(workflowManager, listItem, workflowName, eventData);
		}

		/// <summary>
		/// Updates choices on a field.
		/// </summary>
		/// <param name="ormField">The field to update the choices on.</param>
		/// <param name="choices">The new choices.</param>
		public virtual void SPOrmFieldUpdateChoices(ISPOrmField ormField, IEnumerable<string> choices) {
			SPField field = ormField.GetSPField();
			SPFieldHelper.UpdateChoices(field, choices);
		}

		///<summary>
		/// Allows upsafe updates
		///</summary>
		public virtual bool AllowUnsafeUpdates {
			get { return _site.AllowUnsafeUpdates; }
			set {
				_site.AllowUnsafeUpdates = value;
				_site.Update();
			}
		}

		/// <summary>
		/// Runs in elevated privileges.
		/// </summary>
		/// <typeparam name="TDataContext">The <see cref="SPDataContext"/>.</typeparam>
		/// <typeparam name="TItem">The type of the item.</typeparam>
		/// <param name="obj">The item to run in elevated privileges.</param>
		/// <param name="action">The action to perform.</param>
		public virtual void RunInElevatedSite<TDataContext, TItem>(TItem obj, Action<TDataContext, TItem> action)
			where TDataContext : SPDataContext
			where TItem : ISPOrmObject {
			RunInElevatedSite(obj, false, action);
		}

		/// <summary>
		/// Runs in elevated privileges.
		/// </summary>
		/// <typeparam name="TDataContext">The <see cref="SPDataContext"/>.</typeparam>
		/// <typeparam name="TItem">The type of the item.</typeparam>
		/// <param name="obj">The item to run in elevated privileges.</param>
		/// <param name="allowUnsafeUpdates">Sets AllowUnsafeUpdates to true before the action</param>
		/// <param name="action">The action to perform.</param>
		public virtual void RunInElevatedSite<TDataContext, TItem>(TItem obj, bool allowUnsafeUpdates, Action<TDataContext, TItem> action)
			where TDataContext : SPDataContext
			where TItem : ISPOrmObject {
			SPListItem item = GetSPListItem(obj);
			SPSecurityHelper.RunInElevatedSite(Site, item, (elevSite, elevItem) => {
				using (TDataContext elevDataContext = CreateDataContext<TDataContext>(elevSite)) {
					TItem elemAttachedItem = elevDataContext.Attach<TItem>(elevItem);
					bool prevAllowUnsafeUpdate = false;
					try {
						if (allowUnsafeUpdates) {
							prevAllowUnsafeUpdate = elevDataContext.AllowUnsafeUpdates;
							elevDataContext.AllowUnsafeUpdates = true;
						}
						action(elevDataContext, elemAttachedItem);
					} finally {
						if (allowUnsafeUpdates) {
							elevDataContext.AllowUnsafeUpdates = prevAllowUnsafeUpdate;
						}
					}
				}
			});
		}

		/// <summary>
		/// Runs in elevated privileges.
		/// </summary>
		/// <typeparam name="TDataContext">The <see cref="SPDataContext"/>.</typeparam>
		/// <param name="action">The action to perform.</param>
		public virtual void RunInElevatedSite<TDataContext>(Action<TDataContext> action)
			where TDataContext : SPDataContext {
			RunInElevatedSite(false, action);
		}

		/// <summary>
		/// Runs in elevated privileges.
		/// </summary>
		/// <typeparam name="TDataContext">The <see cref="SPDataContext"/>.</typeparam>
		/// <param name="allowUnsafeUpdates">Sets AllowUnsafeUpdates to true before the action</param>
		/// <param name="action">The action to perform.</param>
		public virtual void RunInElevatedSite<TDataContext>(bool allowUnsafeUpdates, Action<TDataContext> action)
			where TDataContext : SPDataContext {
			SPSecurityHelper.RunInElevatedSite(Site, (elevSite) => {
				using (TDataContext elevDataContext = CreateDataContext<TDataContext>(elevSite)) {
					bool prevAllowUnsafeUpdate = false;
					try {
						if (allowUnsafeUpdates) {
							prevAllowUnsafeUpdate = elevDataContext.AllowUnsafeUpdates;
							elevDataContext.AllowUnsafeUpdates = true;
						}
						action(elevDataContext);
					} finally {
						if (allowUnsafeUpdates) {
							elevDataContext.AllowUnsafeUpdates = prevAllowUnsafeUpdate;
						}
					}
				}
			});
		}

		protected virtual TDataContext CreateDataContext<TDataContext>(SPWeb site) where TDataContext : SPDataContext {
			return (TDataContext)Activator.CreateInstance(this.GetType(), site);
		}

		/// <summary>
		/// Finds a field in the site by group and title.
		/// </summary>
		/// <param name="group">The group the field belongs to.</param>
		/// <param name="title">The name of the field.</param>
		/// <returns>The field if found.</returns>
		public virtual ISPOrmField FindFieldByGroupAndTitle(string group, string title) {
			return new SPOrmSharePointField(SPWebHelper.FindFieldByGroupAndTitle(Site, group, title));
		}

		/// <summary>
		/// Finds a field in the site by group and title. Or, returns null if not found.
		/// </summary>
		/// <param name="group">The group the field belongs to.</param>
		/// <param name="title">The name of the field.</param>
		/// <returns>The field.</returns>
		public virtual ISPOrmField FindFieldByGroupAndTitleOrDefault(string group, string title) {
			SPField field = SPWebHelper.FindFieldByGroupAndTitleOrDefault(Site, group, title);
			if (field == null) {
				return null;
			}
			return new SPOrmSharePointField(field);
		}

		/// <summary>
		/// Gets the field on a content type.
		/// </summary>
		/// <typeparam name="TContentType">The content type to get the field from.</typeparam>
		/// <param name="func">The property to get the backing field from. (eg. o=>o.Title)</param>
		/// <returns>The field.</returns>
		public virtual ISPOrmField GetField<TContentType>(Expression<Func<TContentType, object>> func) where TContentType : ISPOrmContentType {
			SPContentType contentType = GetSPContentType<TContentType>();
			SPOrmFieldAttribute fieldAttr = SPOrmAttributeHelper.GetFieldAttribute(func);
			string fieldName = fieldAttr.Title;
			SPField field = SPContentTypeHelper.FindFieldOrDefault(contentType, fieldName);
			if (field == null) {
				throw new SPOrmException("Could not find field '" + fieldName + "' on content type '" + contentType.Name + "'");
			}
			return new SPOrmSharePointField(field);
		}

		/// <summary>
		/// Gets the field on a list.
		/// </summary>
		/// <typeparam name="TList">The list to get the field from.</typeparam>
		/// <typeparam name="TContentType">The content type to get the field from.</typeparam>
		/// <param name="func">The property to get the backing field from. (eg. o=>o.Title)</param>
		/// <returns>The field.</returns>
		public virtual ISPOrmField GetField<TList, TContentType>(Expression<Func<TContentType, object>> func)
			where TList : ISPOrmList
			where TContentType : ISPOrmContentType {
			SPList list = GetSPList<TList>();
			SPOrmFieldAttribute fieldAttr = SPOrmAttributeHelper.GetFieldAttribute(func);
			string fieldName = fieldAttr.StaticName;
			SPField field = list.FindFieldOrDefault(fieldName);
			if (field == null) {
				throw new SPOrmException("Could not find field '" + fieldName + "' on list '" + list.Title + "'");
			}
			return new SPOrmSharePointField(field);
		}

		/// <summary>
		/// Finds a user by login name or returns null if it's not found.
		/// </summary>
		/// <param name="loginName">The login name of the user.</param>
		/// <returns>The user, or null if not found.</returns>
		public virtual ISPOrmUser FindUserByLoginNameOrDefault(string loginName) {
			SPUser user = SPWebHelper.FindUserByLoginNameOrDefault(Site, loginName);
			if (user == null) {
				return null;
			}
			return new SPOrmSharePointUser(user);
		}


		/// <summary>
		/// Finds a user by login name, if the user cannot be found 
		/// call EnsureUser (in elevated) or returns null if it's not found.
		/// </summary>
		/// <param name="loginName">The login name of the user.</param>
		/// <returns>The user, or null if not found.</returns>
		public virtual ISPOrmUser FindOrEnsureUserByLoginNameOrDefault(string loginName) {
			SPUser user = SPWebHelper.FindUserByLoginNameOrDefault(Site, loginName);
			if (user != null) {
				return new SPOrmSharePointUser(user);
			}
			RunInElevatedSite<SPDataContext>(true, (elevCtx) => {
				user = elevCtx.Site.EnsureUser(loginName);
			});
			if (user != null) {
				return new SPOrmSharePointUser(user);
			}
			return null;
		}

		/// <summary>
		/// sets a site property.
		/// </summary>
		/// <param name="key">the property key</param>
		/// <param name="value">the value to set the property to</param>
		public virtual void SetProperty(string key, string value) {
			var props = Site.Properties;
			props[key] = value;
			props.Update();
		}

		/// <summary>
		/// gets a site property
		/// </summary>
		/// <param name="key">the property key.</param>
		/// <returns>the value of the property</returns>
		public virtual string GetProperty(string key) {
			return Site.Properties[key];
		}

		///<summary>
		/// Finds or adds a site group.
		///</summary>
		///<param name="name">The name of the site group</param>
		///<param name="owner">The owner of the group</param>
		///<param name="defaultUser">The default user.</param>
		///<param name="description">The description of the group</param>
		///<returns>The new or found group</returns>
		public virtual ISPOrmGroup FindOrAddSiteGroup(string name, ISPOrmUser owner, ISPOrmUser defaultUser, string description) {
			return FindOrAddSiteGroup(name, owner, defaultUser, description, false);
		}

		///<summary>
		/// Finds or adds a site group.
		///</summary>
		///<param name="name">The name of the site group</param>
		///<param name="owner">The owner of the group</param>
		///<param name="defaultUser">The default user.</param>
		///<param name="description">The description of the group</param>
		///<param name="runElevated">true, if you would like to run this in elevated privileges</param>
		///<returns>The new or found group</returns>
		public virtual ISPOrmGroup FindOrAddSiteGroup(string name, ISPOrmUser owner, ISPOrmUser defaultUser, string description, bool runElevated) {
			if (runElevated) {
				ISPOrmGroup result = null;
				RunInElevatedSite<SPDataContext>((elevctx) => {
					result = elevctx.FindOrAddSiteGroup(name, owner, defaultUser, description, false);
				});
				return result;
			}

			SPGroup group = SPWebHelper.FindOrAddSiteGroup(
				Site,
				name,
				owner == null ? null : owner.GetSPPrincipal(),
				defaultUser == null ? null : (SPUser)defaultUser.GetSPPrincipal(),
				description);
			return new SPOrmSharePointGroup(group);
		}
        /// <summary>
        /// The site relative url.
        /// </summary>
	    public virtual string ServerRelativeUrl {
            get { return Site.ServerRelativeUrl; }
	    }
	}
}
